package com.ctsi.commons.util;

import com.ctsi.commons.util.collections.FlexibleMapAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * 处理带有参数的string ，在参数中处理${}，类似模版，并支持前缀js:
 * <p>
 * 即采用${js:a+b},则采用js脚本处理
 * 
 * @author yueming
 * 
 */
public class FlexibleStringExpander implements Serializable {
	private static final Logger log = LoggerFactory.getLogger(FlexibleStringExpander.class);
	/**
	 * 
	 */
	private static final long serialVersionUID = 8420856678733778279L;

	protected final String original;

	protected final List<StringElement> stringElements;

	private final String templatToken;



	private final String endTemplatToken;



	public FlexibleStringExpander(final String original) {
		this(original, "${");
	}

	public FlexibleStringExpander(final String original, final String templatToken) {
		this(original, templatToken, "}");
	}

	public FlexibleStringExpander(final String original, final String templatToken, final String endTemplatToken) {
		this.original = original;
		this.templatToken = templatToken;
		this.endTemplatToken = endTemplatToken;
		stringElements = parseString(original);
	}

	public boolean isEmpty() {
		return (this.original == null || this.original.length() == 0);
	}

	public String getOriginal() {
		return this.original;
	}

	/**
	 * 展开字符串，context为替换字符串的context,嵌套的采用fly parse处理
	 * 
	 * @param context
	 *            包含变量的context
	 * @return 处理后字符串
	 */
	public String expandString(Map<String, ? extends Object> context) {
		return this.expandString(context, null);
	}

	/**
	 * 展开字符串，context为替换字符串的context
	 * 
	 * @param context
	 *            包含变量的context
	 * @param locale
	 *            地区
	 * @return 处理后字符串
	 */
	public String expandString(Map<String, ? extends Object> context, Locale locale) {
		StringBuilder expanded = new StringBuilder(1024);

		for (StringElement element : stringElements) {
			element.appendElement(expanded, context, locale);
		}

		String result = expanded.toString();

		return result;
	}

	protected List<StringElement> parseString(final String original) {
		ArrayList<StringElement> strElems = new ArrayList<StringElement>(8);
		if (UtilValidate.isEmpty(original)) {
			return strElems;
		}
		int origLen = original.length();
		
		int start = original.indexOf(templatToken);
		if (start == -1) {
			strElems.add(new ConstantElement(original));
	 
			return strElems;
		}
		int currentInd = 0;
		int end = -1;
		while (start != -1) {
			end = original.indexOf(endTemplatToken, start);
			if (end == -1) {
				log.warn("Found a ${ without a closing } (curly-brace) in the String: {}", original);
				break;
			}
			// 检查转义
			boolean escapedExpression = (start - 1 >= 0 && original.charAt(start - 1) == '\\');
			if (start > currentInd) {
				// append everything from the current index to the start of the expression
				strElems.add(new ConstantElement(original.substring(currentInd, escapedExpression ? start - 1 : start)));
			}

			// Scan for matching closing bracket
			int ptr = original.indexOf(templatToken, start + 2);
			while (ptr != -1 && end != -1 && ptr < end) {
				end = original.indexOf(endTemplatToken, end + 1);
				ptr = original.indexOf(templatToken, ptr + 2);
			}
			if (end == -1) {
				end = origLen;
			}
			String expression = original.substring(start + 2, end);
 
			if (escapedExpression) {
				strElems.add(new ConstantElement(original.substring(start, end + 1)));
			} else if (expression.contains(templatToken)) {
				strElems.add(new NestVariableElement(expression));
			} else if (original.indexOf("js:", start + 2) == start + 2 && !escapedExpression) {

				strElems.add(new ScriptElement(original.substring(start + 5, end)));
			} else if (original.indexOf("bean:", start + 2) == start + 2 && !escapedExpression) {

				strElems.add(new BeanElement(original.substring(start + 7, end)));
			} else {
				strElems.add(new VariableElement(expression));
			}

			// reset the current index to after the expression, and the start to the beginning of the next expression
			currentInd = end + 1;
			if (currentInd > origLen) {
				currentInd = origLen;
			}
			start = original.indexOf(templatToken, currentInd);
		}
		//剩余数据
		if (currentInd < origLen) {
			strElems.add(new ConstantElement(original.substring(currentInd)));
		}
 
		return strElems;
	}

	public interface StringElement extends Serializable {
		void appendElement(StringBuilder buffer, Map<String, ? extends Object> context, Locale locale);
	}

	public class ConstantElement implements StringElement {

		private static final long serialVersionUID = 1L;
		protected final String value;

		public ConstantElement(String value) {
			this.value = value;
		}
		 @Override
		public void appendElement(StringBuilder buffer, Map<String, ? extends Object> context, Locale locale) {
			buffer.append(this.value);
		}

		public String toString() {
			return "ConstantElement: " + value;
		}
	}

	public class ScriptElement implements StringElement {

		private static final long serialVersionUID = 1L;
		final String scriptlet;

		public ScriptElement(String scriptlet) {
			this.scriptlet = scriptlet;
		}

		public String toString() {
			return "ScriptElement: " + scriptlet;
		}
		 @Override
		public void appendElement(StringBuilder buffer, Map<String, ? extends Object> context, Locale locale) {
			@SuppressWarnings("unchecked")
			Object scriptResult = JsUtil.eval(scriptlet, (Map<String, Object>) context);
			if (scriptResult != null) {
				buffer.append(scriptResult.toString());
			} else {
				log.warn("js scriplet evaluated to null [ {} ], got no return so inserting nothing.", scriptlet);
			}

		}
	}

	public class VariableElement implements StringElement {

		private static final long serialVersionUID = 1L;
		protected final FlexibleMapAccessor<Object> fma;

		public VariableElement(String valueName) {
			this.fma = new FlexibleMapAccessor<Object>(valueName);
		}

		public String toString() {
			return "VariableElement: " + fma.getOriginalName();
		}
		 @Override
		public void appendElement(StringBuilder buffer, Map<String, ? extends Object> context, Locale locale) {
			Object retVal = fma.get(context, locale);
			if (retVal != null) {
				buffer.append(retVal.toString());
			} else {
				// otherwise do nothing
			}
		}
	}

	public class NestVariableElement implements StringElement {

		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		protected final List<StringElement> strElems;
		protected final String srcStr;

		protected NestVariableElement(String original) {
			this.strElems = parseString(original);
			this.srcStr = original;
		}

		public String toString() {
			return "NestVariableElement: " + srcStr;
		}

		@Override
		public void appendElement(StringBuilder buffer, Map<String, ? extends Object> context, Locale locale) {
			if (strElems == null) {
				return;
			}
			StringBuilder expr = new StringBuilder(256);
			expr.append(templatToken);
			for (StringElement elem : this.strElems) {
				elem.appendElement(expr, context, locale);
			}
			expr.append(endTemplatToken);
			final FlexibleStringExpander tem=new FlexibleStringExpander(expr.toString(),templatToken,  endTemplatToken);
			
			buffer.append(tem.expandString(context, locale));
		}
	}

	public class BeanElement implements StringElement {

		private static final long serialVersionUID = 1L;
		protected FlexibleMapAccessor<Object> fma;

		protected boolean formatRight = true;
		protected String fieldName;

		public String toString() {
			return "BeanElement:fieldName=" + fieldName;
		}

		public BeanElement(String valueName) {
			int pos = valueName.lastIndexOf('.');
			String parent;
			if (pos < 0) {
				formatRight = false;
				parent = valueName;
				log.warn("error format {},require at least one '.'", valueName);
			} else {
				parent = valueName.substring(0, pos);
				// System.out.println(parent);
				fieldName = valueName.substring(pos + 1);
			}

			// System.out.println(fieldName);

			this.fma = new FlexibleMapAccessor<Object>(parent);
		}
        @Override
		public void appendElement(StringBuilder buffer, Map<String, ? extends Object> context, Locale locale) {
			Object retVal = fma.get(context, locale);
			if (retVal != null) {
				if (formatRight) {
					if (UtilValidate.isNotEmpty(fieldName)) {
						buffer.append(UtilObject.getBeanValue(retVal, fieldName));
					} else {
						log.warn("error fieldName {} ", fieldName);
					}
				} else {
					buffer.append(retVal.toString());
				}

			} else {
				log.trace("result is null");
			}
		}
	}

}
